﻿using Microsoft.Extensions.DependencyInjection;
using Puppy.Autofac;
using Puppy.DependencyInjection;
using Puppy.Internal;
using Puppy.Modularity;
using Puppy.Modularity.PlugIns;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Puppy
{
    public class PuppyApplication : IPuppyApplication
    {
        public IServiceCollection Services { get; }

        public IServiceProvider ServiceProvider { get; private set; }

        public IServiceScope ServiceScope { get; private set; }

        public IReadOnlyList<IPuppyModuleDescriptor> Modules { get; }

        internal PuppyApplication(
            IServiceCollection services,
            Action<PuppyApplicationOptions> optionsAction)
        {
            Check.NotNull(services, nameof(services));

            Services = services;

            services.TryAddObjectAccessor<IServiceProvider>();

            var options = new PuppyApplicationOptions(services);
            optionsAction?.Invoke(options);
            options.UseAutofac();

            services.AddSingleton<IPuppyApplication>(this);
            services.AddSingleton<IModuleContainer>(this);

            services.AddCoreServices();
            services.AddCorePuppyServices(this, options);

            SetPluginFolder(options);

            Modules = LoadModules(services, options);
            ConfigureServices();
        }

        public virtual void Initialize(IServiceProvider serviceProvider = null)
        {
            if (serviceProvider == null)
            {
                ServiceScope = Services.BuildServiceProviderFromFactory().CreateScope();
                SetServiceProvider(ServiceScope.ServiceProvider);
            }
            else
            {
                SetServiceProvider(serviceProvider);
            }

            InitializeModules();
        }

        protected virtual void SetServiceProvider(IServiceProvider serviceProvider)
        {
            ServiceProvider = serviceProvider;
            ServiceProvider.GetRequiredService<ObjectAccessor<IServiceProvider>>().Value = ServiceProvider;
        }

        protected virtual void SetPluginFolder(PuppyApplicationOptions options)
        {
            options.ExcludeAssembly.AddRange(new List<string> { "Microsoft", "Serilog", "xunit", "System" });

            options.PlugInSources.AddFolder(
                AppDomain.CurrentDomain.BaseDirectory, 
                SearchOption.TopDirectoryOnly, 
                assembly =>
                {
                    if (options.ExcludeAssembly.Any(p => assembly.Contains(p)))
                        return false;

                    return true;
                }
            );
        }

        protected virtual void InitializeModules()
        {
            using (var scope = ServiceProvider.CreateScope())
            {
                scope.ServiceProvider
                    .GetRequiredService<IModuleManager>()
                    .InitializeModules(new ServiceProviderAccessorContext(scope.ServiceProvider));
            }
        }

        protected virtual IReadOnlyList<IPuppyModuleDescriptor> LoadModules(IServiceCollection services, PuppyApplicationOptions options)
        {
            return services
                .GetSingletonInstance<IModuleLoader>()
                .LoadModules(
                    services,
                    options.PlugInSources
                );
        }

        protected virtual void ConfigureServices()
        {
            var context = new ServiceCollectionAccessorContext(Services);
            Services.AddSingleton(context);

            foreach (var module in Modules)
            {
                if (module.Instance is PuppyModule puppyModule)
                {
                    puppyModule.ServiceConfigurationContext = context;
                }
            }

            //PreConfigureServices
            foreach (var module in Modules.Where(m => m.Instance is IPreConfigureServices))
            {
                try
                {
                    ((IPreConfigureServices)module.Instance).PreConfigureServices(context);
                }
                catch (Exception ex)
                {
                    throw new PuppyException($"An error occurred during {nameof(IPreConfigureServices.PreConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
                }
            }

            //ConfigureServices
            foreach (var module in Modules)
            {
                if (module.Instance is PuppyModule puppyModule)
                {
                    Services.AddAssembly(module.Type.Assembly);
                }

                try
                {
                    module.Instance.ConfigureServices(context);
                }
                catch (Exception ex)
                {
                    throw new PuppyException($"An error occurred during {nameof(IPuppyModule.ConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
                }
            }

            //PostConfigureServices
            foreach (var module in Modules.Where(m => m.Instance is IPostConfigureServices))
            {
                try
                {
                    ((IPostConfigureServices)module.Instance).PostConfigureServices(context);
                }
                catch (Exception ex)
                {
                    throw new PuppyException($"An error occurred during {nameof(IPostConfigureServices.PostConfigureServices)} phase of the module {module.Type.AssemblyQualifiedName}. See the inner exception for details.", ex);
                }
            }

            foreach (var module in Modules)
            {
                if (module.Instance is PuppyModule puppyModule)
                {
                    puppyModule.ServiceConfigurationContext = null;
                }
            }
        }

        public virtual void Dispose()
        {
            if (ServiceProvider is IDisposable disposableServiceProvider)
            {
                disposableServiceProvider.Dispose();
            }
            ServiceScope?.Dispose();
        }

        public virtual void Shutdown()
        {
            using (var scope = ServiceProvider.CreateScope())
            {
                scope.ServiceProvider
                    .GetRequiredService<IModuleManager>()
                    .ShutdownModules(new ServiceProviderAccessorContext(scope.ServiceProvider));
            }
        }
    }
}
